這篇將會介紹 ES6 推出了新的 Class 語法,在背後的運作還是以原型為基礎 (prototype based) 的繼承。
但要注意 ES6 Class 它並非是完全的語法糖,底下留言區有良葛格可以參考,我這裡也補充一些內容給讀者:
JS classes are not “just syntactic sugar” 這篇文章的作者透過一些特性的舉例來說明為什麼不完全是語法糖。
對於開發者來說,可以使用更簡潔簡單的語法去實作繼承,熟悉以類別為基礎的物件導向程式語言的開發者也能更容易上手。
首先,我們來看個範例,然後一一解說範例中的一些重點。
class Animal {
constructor(name) {
this.name = name;
}
getName() {
console.log("此為父類別的方法");
console.log(this.name);
}
}
class Dog extends Animal{
constructor(name, age) {
super(name);
this.classis = 'Mammalia'; // classic 為拉丁文的綱,Mammalia 則為哺乳綱、哺乳動物的意思
this.age = age;
}
getName() {
console.log("此為子類別的方法", this.name);
}
static seeClassis(pet) {
console.log(pet.classis);
}
set petAge(value) {
this.age = value;
}
get petAge() {
return this.age;
}
}
const myPet = new Dog('Lucky', 5);
myPet.getName(); // "此為子類別的方法" Lucky
Dog.seeClassis(myPet); // "Mammalia"
console.log(myPet.petAge); // 5
myPet.petAge = 6;
console.log(myPet.petAge); // 6
const herPet = new Dog('Buddy', 4);
console.log(myPet.__proto__ === herPet.__proto__); // true
看完程式碼後,以下將會擷取範例程式碼片段一一說明。
範例中,透過了 Class 建立一個類別,而 constructor 建構子是個隨著 class 一同建立並初始化物件的特殊方法,一個類別只能有一個建構子。
class Animal {
constructor(name) {...}
}
extends 能用來繼承類別,要注意的是子類別的方法會蓋掉父類別的同名方法。
而當子類別自身也需要透過 constructor 建立 Properties 時,就需要使用 super,super 會呼叫父類別的建構式,並指定要提供給父類別的值。
super 功能:
第一點例如可以在子類別 Dog 內的 getName 函式加上 super.getName();
,就會執行父類別的 getName 函式。
class Dog extends Animal {
constructor(name, age) {
super(name);
this.classis = 'Mammalia'; // classic 為拉丁文的綱,Mammalia 則為哺乳綱、哺乳動物的意思
this.age = age;
}
getName() {
super.getName(); // 印出 "此為父類別的方法" Lucky
console.log("此為子類別的方法", this.name);
}
}
例如範例的 getName(),讓類別的實體(instance)使用的方法。
getName() {
console.log("此為子類別的方法", this.name);
}
透過 static 這個關鍵字來建立 static method,只存在 class 中,不能被實體所使用,經常被用來建立給類別使用的工具函數。
static seeClassis(pet) {
console.log(pet.classis);
}
// 使用
Dog.seeClassis(myPet); // "Mammalia"
用來取得和修改物件屬性的方法,setter 要有傳入的變數,而 getter 則沒有,使用它們可以封裝內部邏輯。
set petAge(value) {
this.age = value;
}
get petAge() {
return this.age;
}
用來建立物件。
在這個 "#" 語法推出之前,類別內並沒有像其他程式語言中有:
這幾種存取控制的修飾子,基本上所有的類別中的成員都是公開的。
三種修飾子差別:
- public 修飾的屬性或方法是公有的,可以在任何地方被訪問到,預設所有的屬性和方法都是 public 的
- private 修飾的屬性或方法是私有的,不能在宣告它的類別的外部訪問
- protected 修飾的屬性或方法是受保護的,它和 private 類似,區別是它在子類別中也是允許被訪問的
而在 ES2020 出現的新語法,加上 "#" 就能將一個變數或是方法變成私有,私有屬性必須在建立類別時事先宣告。
範例:
class Rectangle {
#height = 0;
#width;
constructor(height, width) {
this.#height = height;
this.#width = width;
}
}
直接舉個例子,也許讀者曾經看過透過 .prototype
的方式去建立一個函式:
Array.prototype.doSomething = function(){
...
}
但這樣會有個問題,如果你有用到其他的函式庫並且剛好有同名的函式,就可能會有功能被影響的風險,讀者可以參考 Stack Overflow - adding custom functions into Array.prototype 的討論,除了文章提到的 Object.defineProperty()
能建立函式外,也可以運用我們這篇學到的 Class。
Array.prototype.myMap = function(fn, thisArg) {
const result = [];
for (let i = 0; i < this.length; i++) {
result.push(fn.call(thisArg || null), this[i], i, this));
}
return result;
};
// 透過建立子類別,並在子類別新增函式
class ExtendedArray extends Array {
myMap(fn, thisArg) {
const result = [];
for (let i = 0; i < this.length; i++) {
result.push(fn.call(thisArg || null), this[i], i, this));
}
return result;
};
}
在介紹那麼多的語法後,我們來思考一些問題,Class 語法帶給了我們什麼幫助呢?
第一就是使用了簡單、閱讀性高、較容易維護的語法去實作物件導向的繼承,也蠻容易看出繼承間的關係,而且其他語言的工程師也相對容易上手。
另外也比 ES5 語法多了更多修飾子(ex: static、#)或是功能可以運用。
在 TypeScript 中,Class 還有更多相關的語法提供給開發者使用,像是有更多的修飾子,public、private、protected 和 readonly,以及 abstract class、implements 等。
因為這個鐵人賽的主題著重在 JS,所以對這些 TS 語法有興趣的讀者可以另外參考 TypeScript 官網對於 Classes 的文件。
Classes - JavaScript - MDN Web Docs
[教學] 深入淺出 JavaScript ES6 Class (類別)
class
最後是基於原型來實現沒有錯,不過也不完全是語法糖。
例如,類別語法的繼承,能夠繼承標準 API,而且內部實作特性以及特殊行為也會被繼承,例如,可以繼承 Array
,子型態實例的 length
行為,能隨著元素數量自動調整等,內部實作特性以及特殊行為也會繼承。
例如,Array.isArray
的判定依據,就是陣列內部實作特性 [[Class]] 的值 'Array'
,在以前,物件的原型若被修改為 Array.prototype
,雖然可以騙過 instanceof
,然而不會被 Array.isArray
判定為 true
,然而透過 class
來繼承 Array
,引擎會繼承內部特性 [[Class]],Array
的子類實例可以被 Array.isArray
判定為 true
。
這裡謝謝良葛格的改正,那看來自己過去學的觀念和網路上很多文章都寫得不夠正確,我這裡也有閱讀了一些文章,會再更新在文章上!
JS classes are not “just syntactic sugar”
Are ES6 classes just syntactic sugar for the prototypal pattern in Javascript?